home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Visual Cafe 3
/
Visual Cafe 3.ISO
/
Vcafe
/
JFC.bin
/
DefaultStyledDocument.java
< prev
next >
Wrap
Text File
|
1998-06-30
|
45KB
|
1,494 lines
/*
* @(#)DefaultStyledDocument.java 1.73 98/04/09
*
* Copyright (c) 1997 Sun Microsystems, Inc. All Rights Reserved.
*
* This software is the confidential and proprietary information of Sun
* Microsystems, Inc. ("Confidential Information"). You shall not
* disclose such Confidential Information and shall use it only in
* accordance with the terms of the license agreement you entered into
* with Sun.
*
* SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE
* SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR ANY DAMAGES
* SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING
* THIS SOFTWARE OR ITS DERIVATIVES.
*
*/
package com.sun.java.swing.text;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.FontMetrics;
import java.util.Hashtable;
import java.util.Stack;
import java.util.Vector;
import java.io.Serializable;
import com.sun.java.swing.Icon;
import com.sun.java.swing.event.*;
import com.sun.java.swing.undo.AbstractUndoableEdit;
import com.sun.java.swing.undo.CannotRedoException;
import com.sun.java.swing.undo.CannotUndoException;
/**
* A document that can be marked up with character and paragraph
* styles in a manner similar to the Rich Text Format. The element
* structure for this document represents style crossings for
* style runs. These style runs are mapped into a paragraph element
* structure (which may reside in some other structure). The
* style runs break at paragraph boundries since logical styles are
* assigned to paragraph boundries.
* <p>
* Warning: serialized objects of this class will not be compatible with
* future swing releases. The current serialization support is appropriate
* for short term storage or RMI between Swing1.0 applications. It will
* not be possible to load serialized Swing1.0 objects with future releases
* of Swing. The JDK1.2 release of Swing will be the compatibility
* baseline for the serialized form of Swing objects.
*
* @author Timothy Prinzing
* @version 1.73 04/09/98
* @see Document
* @see AbstractDocument
*/
public class DefaultStyledDocument extends AbstractDocument implements StyledDocument {
/**
* Constructs a styled document.
*
* @param c the container for the content
* @param styles resources and style definitions which may
* be shared across documents
*/
public DefaultStyledDocument(Content c, StyleContext styles) {
super(c, styles);
buffer = new ElementBuffer(createDefaultRoot());
Style defaultStyle = styles.getStyle(StyleContext.DEFAULT_STYLE);
setLogicalStyle(0, defaultStyle);
}
/**
* Constructs a styled document with the default content
* storage implementation and a shared set of styles.
*
* @param styles the styles
*/
public DefaultStyledDocument(StyleContext styles) {
this(new StringContent(BUFFER_SIZE_DEFAULT), styles);
}
/**
* Constructs a default styled document. This buffers
* input content by a size of <em>BUFFER_SIZE_DEFAULT</em>
* and has a style context that is scoped by the lifetime
* of the document and is not shared with other documents.
*/
public DefaultStyledDocument() {
this(new StringContent(BUFFER_SIZE_DEFAULT), new StyleContext());
}
/**
* Gets the default root element.
*
* @return the root
* @see Document#getDefaultRootElement
*/
public Element getDefaultRootElement() {
return buffer.getRootElement();
}
/**
* Inserts new elements in bulk. This is useful to allow
* parsing with the document in an unlocked state and
* prepare an element structure modification. This method
* takes an array of tokens that describe how to update an
* element structure so the time within a write lock can
* be greatly reduced in an asynchronous update situation.
* <p>
* This method is thread safe, although most Swing methods
* are not. Please see
* <A HREF="http://java.sun.com/products/jfc/swingdoc-archive/threads.html">Threads
* and Swing</A> for more information.
*
* @param offset the starting offset >= 0
* @param data the element data
* @exception BadLocationException for an invalid starting offset
*/
protected void insert(int offset, ElementSpec[] data) throws BadLocationException {
try {
writeLock();
// install the content
Content c = getContent();
int n = data.length;
int pos = offset;
for (int i = 0; i < n; i++) {
ElementSpec es = data[i];
if (es.getLength() > 0) {
c.insertString(pos, new String(es.getArray(), es.getOffset(),
es.getLength()));
pos += es.getLength();
}
}
// build the element structure
int length = pos - offset;
DefaultDocumentEvent evnt =
new DefaultDocumentEvent(offset, length, DocumentEvent.EventType.CHANGE);
buffer.insert(offset, length, data, evnt);
evnt.end();
fireInsertUpdate(evnt);
fireUndoableEditUpdate(new UndoableEditEvent(this, evnt));
} finally {
writeUnlock();
}
}
/**
* Adds a new style into the logical style hierarchy. Style attributes
* resolve from bottom up so an attribute specified in a child
* will override an attribute specified in the parent.
*
* @param nm the name of the style (must be unique within the
* collection of named styles). The name may be null if the style
* is unnamed, but the caller is responsible
* for managing the reference returned as an unnamed style can't
* be fetched by name. An unnamed style may be useful for things
* like character attribute overrides such as found in a style
* run.
* @param parent the parent style. This may be null if unspecified
* attributes need not be resolved in some other style.
* @return the style
*/
public Style addStyle(String nm, Style parent) {
StyleContext styles = (StyleContext) getAttributeContext();
return styles.addStyle(nm, parent);
}
/**
* Removes a named style previously added to the document.
*
* @param nm the name of the style to remove
*/
public void removeStyle(String nm) {
StyleContext styles = (StyleContext) getAttributeContext();
styles.removeStyle(nm);
}
/**
* Fetches a named style previously added.
*
* @param nm the name of the style
* @return the style
*/
public Style getStyle(String nm) {
StyleContext styles = (StyleContext) getAttributeContext();
return styles.getStyle(nm);
}
/**
* Sets the logical style to use for the paragraph at the
* given position. If attributes aren't explicitly set
* for character and paragraph attributes they will resolve
* through the logical style assigned to the paragraph, which
* in turn may resolve through some hierarchy completely
* independent of the element hierarchy in the document.
* <p>
* This method is thread safe, although most Swing methods
* are not. Please see
* <A HREF="http://java.sun.com/products/jfc/swingdoc-archive/threads.html">Threads
* and Swing</A> for more information.
*
* @param pos the offset from the start of the document >= 0
* @param s the logical style to assign to the paragraph, null if none
*/
public void setLogicalStyle(int pos, Style s) {
Element paragraph = getParagraphElement(pos);
if ((paragraph != null) && (paragraph instanceof AbstractElement)) {
try {
writeLock();
StyleChangeUndoableEdit edit = new StyleChangeUndoableEdit((AbstractElement)paragraph, s);
((AbstractElement)paragraph).setResolveParent(s);
int p0 = paragraph.getStartOffset();
int p1 = paragraph.getEndOffset();
DefaultDocumentEvent e =
new DefaultDocumentEvent(p0, p1 - p0, DocumentEvent.EventType.CHANGE);
e.addEdit(edit);
e.end();
fireChangedUpdate(e);
fireUndoableEditUpdate(new UndoableEditEvent(this, e));
} finally {
writeUnlock();
}
}
}
/**
* Fetches the logical style assigned to the paragraph
* represented by the given position.
*
* @param p the location to translate to a paragraph
* and determine the logical style assigned >= 0. This
* is an offset from the start of the document.
* @return the style, null if none
*/
public Style getLogicalStyle(int p) {
Style s = null;
Element paragraph = getParagraphElement(p);
if (paragraph != null) {
AttributeSet a = paragraph.getAttributes();
s = (Style) a.getResolveParent();
}
return s;
}
/**
* Sets attributes for some part of the document.
* A write lock is held by this operation while changes
* are being made, and a DocumentEvent is sent to the listeners
* after the change has been successfully completed.
* <p>
* This method is thread safe, although most Swing methods
* are not. Please see
* <A HREF="http://java.sun.com/products/jfc/swingdoc-archive/threads.html">Threads
* and Swing</A> for more information.
*
* @param offset the offset in the document >= 0
* @param length the length >= 0
* @param s the attributes
* @param replace true if the previous attributes should be replaced
* before setting the new attributes
*/
public void setCharacterAttributes(int offset, int length, AttributeSet s, boolean replace) {
try {
writeLock();
DefaultDocumentEvent changes =
new DefaultDocumentEvent(offset, length, DocumentEvent.EventType.CHANGE);
// split elements that need it
buffer.change(offset, length, changes);
AttributeSet sCopy = s.copyAttributes();
// PENDING(prinz) - this isn't a very efficient way to iterate
int lastEnd = Integer.MAX_VALUE;
for (int pos = offset; pos < (offset + length); pos = lastEnd) {
Element run = getCharacterElement(pos);
lastEnd = run.getEndOffset();
MutableAttributeSet attr = (MutableAttributeSet) run.getAttributes();
changes.addEdit(new AttributeUndoableEdit(run, sCopy, replace));
if (replace) {
attr.removeAttributes(attr);
}
attr.addAttributes(s);
}
changes.end();
fireChangedUpdate(changes);
fireUndoableEditUpdate(new UndoableEditEvent(this, changes));
} finally {
writeUnlock();
}
}
/**
* Sets attributes for a paragraph.
* <p>
* This method is thread safe, although most Swing methods
* are not. Please see
* <A HREF="http://java.sun.com/products/jfc/swingdoc-archive/threads.html">Threads
* and Swing</A> for more information.
*
* @param offset the offset into the paragraph >= 0
* @param length the number of characters affected >= 0
* @param s the attributes
* @param replace whether to replace existing attributes, or merge them
*/
public void setParagraphAttributes(int offset, int length, AttributeSet s,
boolean replace) {
try {
writeLock();
DefaultDocumentEvent changes =
new DefaultDocumentEvent(offset, length, DocumentEvent.EventType.CHANGE);
AttributeSet sCopy = s.copyAttributes();
// PENDING(prinz) - this assumes a particular element structure
Element section = getDefaultRootElement();
int index0 = section.getElementIndex(offset);
int index1 = section.getElementIndex(offset + ((length > 0) ? length - 1 : 0));
for (int i = index0; i <= index1; i++) {
Element paragraph = section.getElement(i);
MutableAttributeSet attr = (MutableAttributeSet) paragraph.getAttributes();
changes.addEdit(new AttributeUndoableEdit(paragraph, sCopy, replace));
if (replace) {
attr.removeAttributes(attr);
}
attr.addAttributes(s);
}
changes.end();
fireChangedUpdate(changes);
fireUndoableEditUpdate(new UndoableEditEvent(this, changes));
} finally {
writeUnlock();
}
}
/**
* Gets a paragraph element.
*
* @param pos the starting offset >= 0
* @return the element
*/
public Element getParagraphElement(int pos) {
Element section = getDefaultRootElement();
int index = section.getElementIndex(pos);
Element paragraph = section.getElement(index);
return paragraph;
}
/**
* Gets a character element based on a position.
*
* @param pos the position in the document >= 0
* @return the element
*/
public Element getCharacterElement(int pos) {
Element e = null;
for (e = getDefaultRootElement(); ! e.isLeaf(); ) {
int index = e.getElementIndex(pos);
e = e.getElement(index);
}
return e;
}
// --- local methods -------------------------------------------------
/**
* Updates document structure as a result of text insertion. This
* will happen within a write lock. This implementation simply
* parses the inserted content for line breaks and builds up a set
* of instructions for the element buffer.
*
* @param chng a description of the document change
* @param attr the attributes
*/
protected void insertUpdate(DefaultDocumentEvent chng, AttributeSet attr) {
int offset = chng.getOffset();
int length = chng.getLength();
if (attr == null) {
attr = SimpleAttributeSet.EMPTY;
}
Element paragraph = getParagraphElement(offset + length);
Element run = paragraph.getElement(paragraph.getElementIndex(offset + length));
AttributeSet pattr = paragraph.getAttributes();
AttributeSet cattr = run.getAttributes();
try {
boolean breakAtStart = false;
boolean breakAtEnd = false;
Segment s = new Segment();
Vector parseBuffer = new Vector();
getText(offset, length, s);
char[] txt = s.array;
int n = s.offset + s.count;
int lastOffset = s.offset;
for (int i = s.offset; i < n; i++) {
if (txt[i] == '\n') {
int breakOffset = i + 1;
parseBuffer.addElement(
new ElementSpec(attr, ElementSpec.ContentType,
breakOffset - lastOffset));
parseBuffer.addElement(
new ElementSpec(null, ElementSpec.EndTagType));
parseBuffer.addElement(
new ElementSpec(pattr, ElementSpec.StartTagType));
lastOffset = breakOffset;
}
}
if (lastOffset < n) {
parseBuffer.addElement(
new ElementSpec(attr, ElementSpec.ContentType,
n - lastOffset));
} else {
breakAtEnd = true;
}
if (offset > 0) {
getText(offset - 1, 1, s);
if (s.array[s.offset] == '\n') {
breakAtStart = true;
ElementSpec spec = new ElementSpec(pattr, ElementSpec.StartTagType);
parseBuffer.insertElementAt(spec, 0);
spec = new ElementSpec(pattr, ElementSpec.EndTagType);
parseBuffer.insertElementAt(spec, 0);
}
}
ElementSpec first = (ElementSpec) parseBuffer.firstElement();
if ((breakAtStart == false) && cattr.isEqual(attr) && (offset > 0)) {
first.setDirection(ElementSpec.JoinPreviousDirection);
}
if (((parseBuffer.size() > 1) ||
(first.getDirection() != ElementSpec.JoinPreviousDirection)) &&
(breakAtEnd == false)) {
ElementSpec last = (ElementSpec) parseBuffer.lastElement();
if (run.getEndOffset() <= (offset + length)) {
cattr = getCharacterElement(offset+length).getAttributes();
}
if (cattr.isEqual(attr)) {
last.setDirection(ElementSpec.JoinNextDirection);
}
}
ElementSpec[] spec = new ElementSpec[parseBuffer.size()];
parseBuffer.copyInto(spec);
buffer.insert(offset, length, spec, chng);
} catch (BadLocationException bl) {
}
}
/**
* Updates document structure as a result of text removal.
*
* @param chng a description of the document change
*/
protected void removeUpdate(DefaultDocumentEvent chng) {
buffer.remove(chng.getOffset(), chng.getLength(), chng);
}
/**
* Creates the root element to be used to represent the
* default document structure.
*
* @return the element base
*/
protected AbstractElement createDefaultRoot() {
// grabs a write-lock for this initialization and
// abandon it during initialization so in normal
// operation we can detect an illegitimate attempt
// to mutate attributes.
writeLock();
BranchElement section = new SectionElement();
BranchElement paragraph = new BranchElement(section, null);
LeafElement brk = new LeafElement(paragraph, null, 0, 1);
Element[] buff = new Element[1];
buff[0] = brk;
paragraph.replace(0, 0, buff);
buff[0] = paragraph;
section.replace(0, 0, buff);
writeUnlock();
return section;
}
/**
* Gets the foreground color from an attribute set.
*
* @param attr the attribute set
* @return the color
*/
public Color getForeground(AttributeSet attr) {
return StyleConstants.getForeground(attr);
}
/**
* Gets the background color from an attribute set.
*
* @param attr the attribute set
* @return the color
*/
public Color getBackground(AttributeSet attr) {
throw new Error("not implemented");
}
/**
* Gets the font from an attribute set.
*
* @param attr the attribute set
* @return the font
*/
public Font getFont(AttributeSet attr) {
StyleContext styles = (StyleContext) getAttributeContext();
return styles.getFont(attr);
}
// --- member variables -----------------------------------------------------------
/**
* The default size of the initial content buffer.
*/
public static final int BUFFER_SIZE_DEFAULT = 4096;
private ElementBuffer buffer;
/**
* Default root element for a document... maps out the
* paragraphs/lines contained.
* <p>
* Warning: serialized objects of this class will not be compatible with
* future swing releases. The current serialization support is appropriate
* for short term storage or RMI between Swing1.0 applications. It will
* not be possible to load serialized Swing1.0 objects with future releases
* of Swing. The JDK1.2 release of Swing will be the compatibility
* baseline for the serialized form of Swing objects.
*/
protected class SectionElement extends BranchElement {
/**
* Creates a new SectionElement.
*/
public SectionElement() {
super(null, null);
}
/**
* Gets the name of the element.
*
* @return the name
*/
public String getName() {
return SectionElementName;
}
}
/**
* Specification for building elements.
* <p>
* Warning: serialized objects of this class will not be compatible with * future swing releases. The current serialization support is appropriate
* for short term storage or RMI between Swing1.0 applications. It will
* not be possible to load serialized Swing1.0 objects with future releases
* of Swing. The JDK1.2 release of Swing will be the compatibility
* baseline for the serialized form of Swing objects.
*/
public static class ElementSpec {
/**
* A possible value for getType. This specifies
* that this record type is a start tag and
* represents markup that specifies the start
* of an element.
*/
public static final short StartTagType = 1;
/**
* A possible value for getType. This specifies
* that this record type is a end tag and
* represents markup that specifies the end
* of an element.
*/
public static final short EndTagType = 2;
/**
* A possible value for getType. This specifies
* that this record type represents content.
*/
public static final short ContentType = 3;
/**
* A possible value for getDirection. This specifies
* that the data associated with this record should
* be joined to what precedes it.
*/
public static final short JoinPreviousDirection = 4;
/**
* A possible value for getDirection. This specifies
* that the data associated with this record should
* be joined to what follows it.
*/
public static final short JoinNextDirection = 5;
/**
* A possible value for getDirection. This specifies
* that the data associated with this record should
* be used to originate a new element. This would be
* the normal value.
*/
public static final short OriginateDirection = 6;
/**
* Constructor useful for markup when the markup will not
* be stored in the document.
*
* @param a the attributes for the element
* @param type the type of the element (StartTagType, EndTagType,
* ContentType)
*/
public ElementSpec(AttributeSet a, short type) {
this(a, type, null, 0, 0);
}
/**
* Constructor for parsing inside the document when
* the data has already been added, but len information
* is needed.
*
* @param a the attributes for the element
* @param type the type of the element (StartTagType, EndTagType,
* ContentType)
* @param len the length >= 0
*/
public ElementSpec(AttributeSet a, short type, int len) {
this(a, type, null, 0, len);
}
/**
* Constructor for creating a spec externally for batch
* input of content and markup into the document.
*
* @param a the attributes for the element
* @param type the type of the element (StartTagType, EndTagType,
* ContentType)
* @param txt the text for the element
* @param offs the offset into the text >= 0
* @param len the length of the text >= 0
*/
public ElementSpec(AttributeSet a, short type, char[] txt,
int offs, int len) {
attr = a;
this.type = type;
this.data = txt;
this.offs = offs;
this.len = len;
this.direction = OriginateDirection;
}
/**
* Sets the element type.
*
* @param type the type of the element (StartTagType, EndTagType,
* ContentType)
*/
public void setType(short type) {
this.type = type;
}
/**
* Gets the element type.
*
* @return the type of the element (StartTagType, EndTagType,
* ContentType)
*/
public short getType() {
return type;
}
/**
* Sets the direction.
*
* @param direction the direction (JoinPreviousDirection,
* JoinNextDirection)
*/
public void setDirection(short direction) {
this.direction = direction;
}
/**
* Gets the direction.
*
* @return the direction (JoinPreviousDirection, JoinNextDirection)
*/
public short getDirection() {
return direction;
}
/**
* Gets the element attributes.
*
* @return the attribute set
*/
public AttributeSet getAttributes() {
return attr;
}
/**
* Gets the array of characters.
*
* @return the array
*/
public char[] getArray() {
return data;
}
/**
* Gets the starting offset.
*
* @return the offset >= 0
*/
public int getOffset() {
return 0;
}
/**
* Gets the length.
*
* @return the length >= 0
*/
public int getLength() {
return len;
}
/**
* Converts the element to a string.
*
* @return the string
*/
public String toString() {
String tlbl = "??";
String plbl = "??";
switch(type) {
case StartTagType:
tlbl = "StartTag";
break;
case ContentType:
tlbl = "Content";
break;
case EndTagType:
tlbl = "EndTag";
break;
}
switch(direction) {
case JoinPreviousDirection:
plbl = "JoinPrevious";
break;
case JoinNextDirection:
plbl = "JoinNext";
break;
case OriginateDirection:
plbl = "Originate";
break;
}
return tlbl + ":" + plbl + ":" + getLength();
}
private AttributeSet attr;
private int len;
private short type;
private short direction;
private int offs;
private char[] data;
}
/**
* Class to manage changes to the element
* hierarchy.
* <p>
* Warning: serialized objects of this class will not be compatible with
* future swing releases. The current serialization support is appropriate
* for short term storage or RMI between Swing1.0 applications. It will
* not be possible to load serialized Swing1.0 objects with future releases
* of Swing. The JDK1.2 release of Swing will be the compatibility
* baseline for the serialized form of Swing objects.
*/
public class ElementBuffer implements Serializable {
/**
* Creates a new ElementBuffer.
*
* @param root the root element
*/
public ElementBuffer(Element root) {
this.root = root;
changes = new Vector();
path = new Stack();
endJoin = new Vector();
}
/**
* Gets the root element.
*
* @return the root element
*/
public Element getRootElement() {
return root;
}
/**
* Inserts new content.
*
* @param offset the starting offset >= 0
* @param length the length >= 0
* @param data the data to insert
* @param de the event capturing this edit
*/
public final void insert(int offset, int length, ElementSpec[] data,
DefaultDocumentEvent de) {
insertOp = true;
beginEdits(offset, length);
insertUpdate(data);
endEdits(de);
insertOp = false;
}
/**
* Removes content.
*
* @param offset the starting offset >= 0
* @param length the length >= 0
* @param de the event capturing this edit
*/
public final void remove(int offset, int length, DefaultDocumentEvent de) {
beginEdits(offset, length);
removeUpdate();
endEdits(de);
}
/**
* Changes content.
*
* @param offset the starting offset >= 0
* @param length the length >= 0
* @param de the event capturing this edit
*/
public final void change(int offset, int length, DefaultDocumentEvent de) {
beginEdits(offset, length);
changeUpdate();
endEdits(de);
}
/**
* Inserts an update into the document.
*
* @param data the elements to insert
*/
protected void insertUpdate(ElementSpec[] data) {
// push the path
Element elem = root;
int index = elem.getElementIndex(offset);
while (! elem.isLeaf()) {
Element child = elem.getElement(index);
push(elem, (child.isLeaf() ? index : index+1));
elem = child;
index = elem.getElementIndex(offset);
}
// open a hole to inject new elements (if needed)
open(data);
// fold in the specified subtree
int n = data.length;
for (int i = 0; i < n; i++) {
insertElement(data[i]);
}
// close up the hole in the tree
close();
// pop the remaining path
while (path.size() != 0) {
pop();
}
}
/**
* Updates the element structure in response to a removal from the
* associated sequence in the document. Any elements consumed by the
* span of the removal are removed.
*/
protected void removeUpdate() {
removeElements(root, offset, offset + length);
}
/**
* Updates the element structure in response to a change in the
* document.
*/
protected void changeUpdate() {
boolean didEnd = split(offset, length);
if (! didEnd) {
// need to do the other end
while (path.size() != 0) {
pop();
}
split(offset + length, 0);
}
while (path.size() != 0) {
pop();
}
}
boolean split(int offs, int len) {
boolean splitEnd = false;
// push the path
Element e = root;
int index = e.getElementIndex(offs);
while (! e.isLeaf()) {
push(e, index);
e = e.getElement(index);
index = e.getElementIndex(offs);
}
ElemChanges ec = (ElemChanges) path.peek();
Element child = ec.parent.getElement(ec.index);
// make sure there is something to do... if the
// offset is already at a boundry then there is
// nothing to do.
if (child.getStartOffset() != offs) {
// we need to split, now see if the other end is within
// the same parent.
int index0 = ec.index;
int index1 = index0;
if (((offs + len) < ec.parent.getEndOffset()) && (len != 0)) {
// it's a range split in the same parent
index1 = ec.parent.getElementIndex(offs+len);
if (index1 == index0) {
// it's a three-way split
ec.removed.addElement(child);
e = createLeafElement(ec.parent, child.getAttributes(),
child.getStartOffset(), offs);
ec.added.addElement(e);
e = createLeafElement(ec.parent, child.getAttributes(),
offs, offs + len);
ec.added.addElement(e);
e = createLeafElement(ec.parent, child.getAttributes(),
offs + len, child.getEndOffset());
ec.added.addElement(e);
return true;
} else {
child = ec.parent.getElement(index1);
if ((offs + len) == child.getStartOffset()) {
// end is already on a boundry
index1 = index0;
}
}
splitEnd = true;
}
// split the first location
pos = offs;
child = ec.parent.getElement(index0);
ec.removed.addElement(child);
e = createLeafElement(ec.parent, child.getAttributes(),
child.getStartOffset(), pos);
ec.added.addElement(e);
e = createLeafElement(ec.parent, child.getAttributes(),
pos, child.getEndOffset());
ec.added.addElement(e);
// pick up things in the middle
for (int i = index0 + 1; i < index1; i++) {
child = ec.parent.getElement(i);
ec.removed.addElement(child);
ec.added.addElement(child);
}
if (index1 != index0) {
child = ec.parent.getElement(index1);
pos = offs + len;
ec.removed.addElement(child);
e = createLeafElement(ec.parent, child.getAttributes(),
child.getStartOffset(), pos);
ec.added.addElement(e);
e = createLeafElement(ec.parent, child.getAttributes(),
pos, child.getEndOffset());
ec.added.addElement(e);
}
}
return splitEnd;
}
/**
* Creates the UndoableEdit record for the edits made
* in the buffer.
*/
void endEdits(DefaultDocumentEvent de) {
int n = changes.size();
for (int i = 0; i < n; i++) {
ElemChanges ec = (ElemChanges) changes.elementAt(i);
Element[] removed = new Element[ec.removed.size()];
ec.removed.copyInto(removed);
Element[] added = new Element[ec.added.size()];
ec.added.copyInto(added);
int index = ec.index;
((BranchElement) ec.parent).replace(index, removed.length, added);
ElementEdit ee = new ElementEdit((BranchElement) ec.parent,
index, removed, added);
de.addEdit(ee);
}
/*
for (int i = 0; i < n; i++) {
ElemChanges ec = (ElemChanges) changes.elementAt(i);
System.err.print("edited: " + ec.parent + " at: " + ec.index +
" removed " + ec.removed.size());
if (ec.removed.size() > 0) {
int r0 = ((Element) ec.removed.firstElement()).getStartOffset();
int r1 = ((Element) ec.removed.lastElement()).getEndOffset();
System.err.print("[" + r0 + "," + r1 + "]");
}
System.err.print(" added " + ec.added.size());
if (ec.added.size() > 0) {
int p0 = ((Element) ec.added.firstElement()).getStartOffset();
int p1 = ((Element) ec.added.lastElement()).getEndOffset();
System.err.print("[" + p0 + "," + p1 + "]");
}
System.err.println("");
}
*/
}
/**
* Initialize the buffer
*/
void beginEdits(int offset, int length) {
this.offset = offset;
this.length = length;
pos = offset;
if (changes == null) {
changes = new Vector();
} else {
changes.removeAllElements();
}
if (path == null) {
path = new Stack();
} else {
path.removeAllElements();
}
if (endJoin == null) {
endJoin = new Vector();
} else {
endJoin.removeAllElements();
}
}
/**
* Pushes a new element onto the stack that represents
* the current path.
* @param record Whether or not the push should be
* recorded as an element change or not.
*/
void push(Element e, int index) {
ElemChanges old = (ElemChanges) ((path.size() != 0) ? path.peek() : null);
ElemChanges ec = new ElemChanges(e, index);
path.push(ec);
}
void pop() {
ElemChanges ec = (ElemChanges) path.peek();
path.pop();
if ((ec.added.size() > 0) || (ec.removed.size() > 0)) {
changes.addElement(ec);
} else if (! path.isEmpty()) {
// if we pushed a branch element that didn't get
// used, make sure its not marked as having been added.
Element e = ec.parent;
ec = (ElemChanges) path.peek();
ec.added.removeElement(e);
}
}
/**
* move the current offset forward by n.
*/
void advance(int n) {
pos += n;
}
void insertElement(ElementSpec es) {
ElemChanges ec = (ElemChanges) path.peek();
switch(es.getType()) {
case ElementSpec.StartTagType:
Element belem = createBranchElement(ec.parent, es.getAttributes());
ec.added.addElement(belem);
push(belem, 0);
break;
case ElementSpec.EndTagType:
pop();
break;
case ElementSpec.ContentType:
int len = es.getLength();
if (es.getDirection() != ElementSpec.JoinPreviousDirection) {
Element leaf = createLeafElement(ec.parent, es.getAttributes(),
pos, pos + len);
ec.added.addElement(leaf);
}
pos += len;
break;
}
}
void removeElements(Element elem, int rmOffs0, int rmOffs1) {
if (! elem.isLeaf()) {
// update path for changes
int index0 = elem.getElementIndex(rmOffs0);
int index1 = elem.getElementIndex(rmOffs1);
push(elem, index0);
// if the range is contained by one element,
// we just forward the request
if (index0 == index1) {
removeElements(elem.getElement(index0), rmOffs0, rmOffs1);
} else {
// the removal range spans elements. If we can join
// the two endpoints, do it. Otherwise we remove the
// interior and forward to the endpoints.
Element child0 = elem.getElement(index0);
Element child1 = elem.getElement(index1);
ElemChanges ec = (ElemChanges) path.peek();
if (canJoin(child0, child1)) {
// remove and join
for (int i = index0; i <= index1; i++) {
ec.removed.addElement(elem.getElement(i));
}
Element e = join(elem, child0, child1, rmOffs0, rmOffs1);
ec.added.addElement(e);
} else {
// remove interior and forward
int rmIndex0 = index0 + 1;
int rmIndex1 = index1 - 1;
if (child0.getStartOffset() == rmOffs0) {
// start element completely consumed
child0 = null;
rmIndex0 = index0;
}
if (child1.getStartOffset() == rmOffs1) {
// end element not touched
child1 = null;
}
if (rmIndex0 <= rmIndex1) {
ec.index = rmIndex0;
}
for (int i = rmIndex0; i <= rmIndex1; i++) {
ec.removed.addElement(elem.getElement(i));
}
if (child0 != null) {
removeElements(child0, rmOffs0, rmOffs1);
}
if (child1 != null) {
removeElements(child1, rmOffs0, rmOffs1);
}
}
}
// publish changes
pop();
}
}
/**
* Can the two given elements be coelesced together
* into one element?
*/
boolean canJoin(Element e0, Element e1) {
if ((e0 == null) || (e1 == null)) {
return false;
}
if (e0.getName().equals(ParagraphElementName) &&
e1.getName().equals(ParagraphElementName)) {
return true;
}
return e0.getAttributes().isEqual(e1.getAttributes());
}
/**
* Joins the two elements carving out a hole for the
* given removed range.
*/
Element join(Element p, Element left, Element right, int rmOffs0, int rmOffs1) {
if (left.isLeaf() && right.isLeaf()) {
return createLeafElement(p, left.getAttributes(), left.getStartOffset(),
right.getEndOffset());
} else if ((!left.isLeaf()) && (!right.isLeaf())) {
// join two branch elements. This copies the children before
// the removal range on the left element, and after the removal
// range on the right element. The two elements on the edge
// are joined if possible and needed.
Element to = createBranchElement(p, left.getAttributes());
int ljIndex = left.getElementIndex(rmOffs0);
int rjIndex = right.getElementIndex(rmOffs1);
Element lj = left.getElement(ljIndex);
if (lj.getStartOffset() == rmOffs0) {
lj = null;
}
Element rj = right.getElement(rjIndex);
if (rj.getStartOffset() == rmOffs1) {
rj = null;
}
Vector children = new Vector();
// transfer the left
for (int i = 0; i < ljIndex; i++) {
children.addElement(clone(to, left.getElement(i)));
}
// transfer the join/middle
if (canJoin(lj, rj)) {
Element e = join(to, lj, rj, rmOffs0, rmOffs1);
children.addElement(e);
} else {
if (lj != null) {
children.addElement(clone(to, lj));
}
if (rj != null) {
children.addElement(clone(to, rj));
}
}
// transfer the right
int n = right.getElementCount();
for (int i = (rj == null) ? rjIndex : rjIndex + 1; i < n; i++) {
children.addElement(clone(to, right.getElement(i)));
}
// install the children
Element[] c = new Element[children.size()];
children.copyInto(c);
((BranchElement)to).replace(0, 0, c);
return to;
} else {
throw new StateInvariantError(
"No support to join leaf element with non-leaf element");
}
}
/**
* Creates a copy of this element, with a different
* parent.
*
* @param parent the parent element
* @param clonee the element to be cloned
* @return the copy
*/
public Element clone(Element parent, Element clonee) {
if (clonee.isLeaf()) {
return createLeafElement(parent, clonee.getAttributes(),
clonee.getStartOffset(),
clonee.getEndOffset());
}
Element e = createBranchElement(parent, clonee.getAttributes());
int n = clonee.getElementCount();
Element[] children = new Element[n];
for (int i = 0; i < n; i++) {
children[i] = clone(e, clonee.getElement(i));
}
((BranchElement)e).replace(0, 0, children);
return e;
}
/**
* open the tree to fold in this data
*/
void open(ElementSpec[] data) {
int firstOffs = (data[0].getDirection() == ElementSpec.JoinPreviousDirection) ?
offset + data[0].getLength() : offset;
int lastOffs = offset + length;
// FIXME - should actually join when end is a JoinNextDirection
ElemChanges ec = (ElemChanges) path.peek();
Element child = ec.parent.getElement(ec.index);
boolean hasPop = false;
for (int i = 0; i < data.length; i++) {
if (data[i].getType() == ElementSpec.EndTagType) {
hasPop = true;
break;
}
}
if (hasPop || (firstOffs != lastOffs)) {
// split the entry point
ec.removed.addElement(child);
if (firstOffs != child.getStartOffset()) {
// put the area before the insertion back into
// the tree (ie the part before the split).
Element shortened =
createLeafElement(ec.parent, child.getAttributes(),
child.getStartOffset(), firstOffs);
ec.added.addElement(shortened);
}
if (child.getEndOffset() > lastOffs) {
// pick up the content after the hole to be returned
// back to the tree.
int len = child.getEndOffset() - lastOffs;
ElementSpec spec =
new ElementSpec(child.getAttributes(),
ElementSpec.ContentType, len);
endJoin.addElement(spec);
}
if (hasPop) {
// pick up the remaining content to be placed into a new
// parent.
int n = ec.parent.getElementCount();
for (int i = ec.index + 1; i < n; i++) {
child = ec.parent.getElement(i);
ec.removed.addElement(child);
int len = child.getEndOffset() - child.getStartOffset();
ElementSpec spec =
new ElementSpec(child.getAttributes(),
ElementSpec.ContentType, len);
endJoin.addElement(spec);
}
}
if (hasPop && (endJoin.size() == 0)) {
// join into next paragraph, pull the next paragraph
// from the tree.
ec = (ElemChanges) path.elementAt(path.size() - 2);
Element e = ec.parent.getElement(ec.index);
if (e != null) {
ec.removed.addElement(e);
int n = e.getElementCount();
for (int i = 0; i < n; i++) {
child = e.getElement(i);
int len = child.getEndOffset() - child.getStartOffset();
ElementSpec spec =
new ElementSpec(child.getAttributes(),
ElementSpec.ContentType, len);
endJoin.addElement(spec);
}
}
}
}
}
/**
* finish mending the right side of the subtree
* inserted into the element hierarchy.
*/
void close() {
ElemChanges ec = (ElemChanges) path.peek();
int n = endJoin.size();
for (int i = 0; i < n; i++) {
ElementSpec spec = (ElementSpec) endJoin.elementAt(i);
int p1 = pos + spec.getLength();
Element e = createLeafElement(ec.parent, spec.getAttributes(), pos, p1);
ec.added.addElement(e);
pos = p1;
}
}
Element root;
transient int pos; // current position
transient int offset;
transient int length;
transient Vector endJoin; // Vector<ElementSpec>
transient Vector changes; // Vector<ElemChanges>
transient Stack path; // Stack<ElemChanges>
transient boolean insertOp;
/*
* Internal record used to hold element change specifications
*/
class ElemChanges {
ElemChanges(Element parent, int index) {
this.parent = parent;
this.index = index;
added = new Vector();
removed = new Vector();
}
public String toString() {
return "added: " + added + "\nremoved: " + removed + "\n";
}
Element parent;
int index;
Vector added;
Vector removed;
}
}
/**
* An UndoableEdit used to remember AttributeSet changes to an
* Element.
*/
static class AttributeUndoableEdit extends AbstractUndoableEdit {
AttributeUndoableEdit(Element element, AttributeSet newAttributes,
boolean isReplacing) {
super();
this.element = element;
this.newAttributes = newAttributes;
this.isReplacing = isReplacing;
// If not replacing, it may be more efficient to only copy the
// changed values...
copy = element.getAttributes().copyAttributes();
}
/**
* Redoes a change.
*
* @exception CannotRedoException if the change cannot be redone
*/
public void redo() throws CannotRedoException {
super.redo();
MutableAttributeSet as = (MutableAttributeSet)element
.getAttributes();
if(isReplacing)
as.removeAttributes(as);
as.addAttributes(newAttributes);
}
/**
* Undoes a change.
*
* @exception CannotUndoException if the change cannot be undone
*/
public void undo() throws CannotUndoException {
super.undo();
MutableAttributeSet as = (MutableAttributeSet)element.getAttributes();
as.removeAttributes(newAttributes);
as.addAttributes(copy);
}
// AttributeSet containing additional entries, must be non-mutable!
protected AttributeSet newAttributes;
// Copy of the AttributeSet the Element contained.
protected AttributeSet copy;
// true if all the attributes in the element were removed first.
protected boolean isReplacing;
// Efected Element.
protected Element element;
}
/**
* UndoableEdit for changing the resolve parent of an Element.
*/
static class StyleChangeUndoableEdit extends AbstractUndoableEdit {
public StyleChangeUndoableEdit(AbstractElement element,
Style newStyle) {
super();
this.element = element;
this.newStyle = newStyle;
oldStyle = element.getResolveParent();
}
/**
* Redoes a change.
*
* @exception CannotRedoException if the change cannot be redone
*/
public void redo() throws CannotRedoException {
super.redo();
element.setResolveParent(newStyle);
}
/**
* Undoes a change.
*
* @exception CannotUndoException if the change cannot be undone
*/
public void undo() throws CannotUndoException {
super.undo();
element.setResolveParent(oldStyle);
}
/** Element to change resolve parent of. */
protected AbstractElement element;
/** New style. */
protected Style newStyle;
/** Old style, before setting newStyle. */
protected AttributeSet oldStyle;
}
}